﻿using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;

namespace Firefly.Net {
    public class TelnetClient : TcpClient {

        public void Negotiate(byte peek) {
            NetworkStream stream = GetStream();
            List<byte> data = new List<byte>();
            data.Add((byte)NegotiationCode.IAC);
            Console.WriteLine("Telnet Negotiation Started: " + peek);
            short panicOverflow = 0;
            bool endRead = false;
            while (!endRead) {
                panicOverflow++;
                if (panicOverflow > 5) {
                    Console.WriteLine("PANIC OVERFLOW IN TELNET NEGOTIATION");
                    endRead = true;
                }
                data.Add((byte)stream.ReadByte()); // Read byte to get negotiation.
                if (data[data.Count - 1] == (byte)NegotiationCode.DO) {
                    data.Add((byte)stream.ReadByte()); // Read next byte to get service
                    foreach (byte b in data)
                        Console.WriteLine(String.Format("Rcvd: {0:G} (" + (int)b + ")", (AllCodes)b));
                    switch (data[data.Count - 1]) {
                        //case (byte)TelnetCode.NAWS:
                        case (byte)TelnetCode.TERMTYPE:
                        case (byte)TelnetCode.LineMode:
                        //case (byte)TelnetCode.TERMSPEED:
                            Console.WriteLine("Caught MODE.");
                            SendCommand(NegotiationCode.WILL, (TelnetCode)data[data.Count - 1]);
                            endRead = true;
                            break;
                        default:
                            Console.WriteLine("Encountered unknown protocol.");
                            SendCommand(NegotiationCode.WONT, data[data.Count - 1]);
                            endRead = true;
                            break;
                    }
                } else if (data[data.Count - 1] == (byte)NegotiationCode.SB) {
                    data.Add((byte)stream.ReadByte()); // Read next byte to get service
                    Console.WriteLine(String.Format("Subnegotiations started for: {0:G}", (TelnetCode)data[data.Count - 1]));
                    SubNegotiate((TelnetCode)data[data.Count - 1], stream);
                    endRead = true;
                } else if (data[data.Count - 1] == (byte)NegotiationCode.SE) {
                    endRead = true;
                } else if (data[data.Count - 1] == (byte)NegotiationCode.WONT) {
                    data.Add((byte)stream.ReadByte()); // Read next byte to get service
                    Console.WriteLine(String.Format("Server doesn't support: {0:G}", (TelnetCode)data[data.Count - 1]));
                    endRead = true;
                    break;
                } else if (data[data.Count - 1] == (byte)NegotiationCode.WILL) {
                    data.Add((byte)stream.ReadByte()); // Read next byte to get service
                    Console.WriteLine("Caught MODE.");
                    SendCommand(NegotiationCode.DO, (TelnetCode)data[data.Count - 1]);
                    endRead = true;
                            break;
                } else {
                    Console.WriteLine(String.Format("WARNING UNKNOWN BYTE: {0:G} (" + data[data.Count - 1] + ")",
                        (AllCodes)data[data.Count - 1]));
                    endRead = true;
                }
            }
        }

        public void SubNegotiate(TelnetCode mode, NetworkStream stream) {
            byte subCommand = (byte)stream.ReadByte();
            Console.WriteLine(String.Format("Subnegotiation Var: {0:G} (" + (int)subCommand + ") or " + (char)subCommand, (AllCodes)subCommand));

            if (subCommand == (byte)NegotiationCode.SEND) {
                switch (mode) {
                    case TelnetCode.LineMode:
                        int modeMask = stream.ReadByte(); // For PENN this is byte 0x09.
                        SendCommand(mode, new byte[] { 0x01, (byte)modeMask }); // Edit Mode
                        break;
                    //case TelnetCode.NAWS:
                    //    SendCommand(mode, new byte[] { 78, 78, 78, 78 });
                    //    break;
                    case TelnetCode.MSSP:
                        ProcessMSSP(subCommand, stream);
                        break;
                    case TelnetCode.TERMTYPE:
                        //case TelnetCode.TERMSPEED:
                        SendCommand(mode, Encoding.UTF8.GetBytes("Firefly"));
                        break;
                    default:
                        Console.WriteLine(String.Format("Encountered unknown subnegotiation: {0:G} (" + (int)mode + ")", (AllCodes)mode));
                        break;
                }
            } else {
                Console.WriteLine(String.Format("Hit an unknown subnegotiation code: {0:G} (" + subCommand + ")",
                            (AllCodes)subCommand));
            }
        }

        /// <summary>
        /// Process MSSP stream. It's like subnegotiations in my subnegotiations.
        /// </summary>
        /// <param name="lastByte"></param>
        /// <param name="stream"></param>
        private void ProcessMSSP(byte lastByte, NetworkStream stream) {
            byte subCommand = lastByte;

            bool msspReadMode = true;
            while (msspReadMode) {
                switch (subCommand) {
                    case (byte)MSSPVariable.MSSP_VAL:
                    case (byte)MSSPVariable.MSSP_VAR:
                        bool reading = true;
                        string data = String.Empty;
                        // Read the byte
                        while (reading) {
                            byte b = (byte)stream.ReadByte();

                            if (b != (byte)MSSPVariable.MSSP_VAR
                                && b != (byte)MSSPVariable.MSSP_VAL
                                && b != (byte)NegotiationCode.IAC) {
                                // Write a byte of data to the string.
                                data += (char)b;
                            } else {
                                Console.WriteLine("MSSP Sent: " + data);
                                // Clear the string.
                                data = String.Empty;
                                // Next command is likely an IAC/VAR/VAL bit.
                                subCommand = b;
                                // Restart the loop, read next data.
                                reading = false;
                            }
                        }
                        break;
                    case (byte)NegotiationCode.IAC:
                        if (stream.ReadByte() == (int)NegotiationCode.SE)
                            msspReadMode = false;
                        break;
                    default:
                        Console.WriteLine(String.Format("MSSP Subnegotiation hit an unknown byte: {0:G} (" + subCommand + ")",
                            (AllCodes)subCommand));
                        msspReadMode = false;
                        break;
                }
            }
        }

        /// <summary>
        /// Send a command response to the server.
        /// </summary>
        /// <param name="Option"></param>
        /// <param name="Mode"></param>
        public void SendCommand(NegotiationCode Option, TelnetCode Mode) {
            NetworkStream stream = GetStream();
            byte[] streamData = new byte[] {
                (byte)NegotiationCode.IAC,
                (byte)Option,
                (byte)Mode
            };
            stream.Write(streamData, 0, 3);
            foreach (byte b in streamData)
                Console.WriteLine(String.Format("Sent: {0:G} (" + (int)b + ")", (AllCodes)b));
        }

        /// <summary>
        /// Send a command to the server using a byte.
        /// </summary>
        /// <param name="Option"></param>
        /// <param name="Mode"></param>
        public void SendCommand(NegotiationCode Option, byte Mode) {
            NetworkStream stream = GetStream();
            byte[] streamData = new byte[] {
                (byte)NegotiationCode.IAC,
                (byte)Option,
                Mode
            };
            stream.Write(streamData, 0, 3);
            foreach (byte b in streamData)
                Console.WriteLine(String.Format("Sent: {0:G} (" + (int)b + ")", (AllCodes)b));
        }

        /// <summary>
        /// Send a stream of data to the server. Commonly used for subnegotiations.
        /// </summary>
        /// <param name="Mode"></param>
        /// <param name="data"></param>
        public void SendCommand(TelnetCode Mode, byte[] data) {
            NetworkStream stream = GetStream();
            byte[] streamData = new byte[6 + data.Length];
            streamData[0] = (byte)NegotiationCode.IAC;
            streamData[1] = (byte)NegotiationCode.SB;
            streamData[2] = (byte)Mode;
            data.CopyTo(streamData, 4);
            streamData[data.Length + 4] = (byte)NegotiationCode.IAC;
            streamData[data.Length + 5] = (byte)NegotiationCode.SE;
            Console.WriteLine("Sent Data Callback:");
            foreach (byte b in streamData)
                Console.WriteLine(String.Format("Sent: {0:G} (" + (int)b + ")", (AllCodes)b));
            Console.WriteLine("Sent Data Callback End");
            stream.Write(streamData, 0, streamData.Length);
        }
    }

    // 0xff 0xfa 0x22 MODE MASK 0xff 0xf0
    // IAC SB LINEMODE MODE mask IAC SE
    public enum TelnetCode : byte {
        NULL = 0x00,
        ECHO = 0x01,
        LineFeed = 0x0a,
        NAWS = 0x1F,
        TERMSPEED = 0x20,
        LineMode = 0x22,
        MSSP = 0x46, // Mud service protocol
        SUPPRESS_GO_AHEAD = 0x04,
        TERMTYPE = 0x18,
    }

    public enum NegotiationCode : byte {
        IS = 0x00,
        SEND = 0x01,
        SE = 0xf0, // subnegotiation end
        SB = 0xfa, // subnegotiation start
        WILL = 0xfb,
        WONT = 0xfc,
        DO = 0xfd,
        DONT = 0xfe,
        IAC = 0xff
    }

    public enum AllCodes : byte {
        IS = 0x00,
        SEND = 0x01,
        SE = 0xf0, // subnegotiation end
        SB = 0xfa, // subnegotiation start
        WILL = 0xfb,
        WONT = 0xfc,
        DO = 0xfd,
        DONT = 0xfe,
        IAC = 0xff,
        LineFeed = 0x0a,
        NAWS = 0x1F,
        //TERMSPEED = 0x20,
        LineMode = 0x22,
        MSSP = 0x44, // Mud service protocol
        SUPPRESS_GO_AHEAD = 0x04,
        TERMTYPE = 0x18,
    }

    public enum LineModeMaskCode : byte {
        DEFAULT = 0x00,
        EDIT = 0x01,
        TRAPSIG = 0x02,
        MODE_ACK = 0x03
    }

    public enum MSSPVariable : byte {
        MSSP_VAR = 0x01,
        MSSP_VAL = 0x02
    }
}
